Client-Client間のRPCのここが嫌だ
概要
iOSとかで動く対戦型で二人以上のプレイヤーが動くゲームでのClient-Client間のRPCは使いどころ無いので死ねみたいな話。
主にself-definedでないRPCについての文句。
Remote Procedure Call
定義
メソッドシグニチャとかを送って他Peerのメソッドをぶっ叩くこと。
結論から言うと、P2Pでない対戦ゲームでClient-ClientのRPCを使うべきでは無いと思っている。
代表的な嫌なことは下記。代案もある。
Version差で、Client-Serverが引きずられる
独自定義した「わけではない」バイナリを送る感じになるので、クライアント間でのバージョン互換性を保持しないとゲームにならない。
= 全ユーザーのバージョンを保たないといけない
= サーバ側もその制約を被る。
-> いろんなバージョンのクライアントの面倒をみる羽目になる
それは詰み。
データサイズが制御できない
結論から言うとデータ形式をゲームごとに自分で定義したほうがいい。
行動コード + 発行者コード + … とか、すべてbyteで書き、できるだけサイズを小さくする。
っていうのが理想だけど、
開発中は別に中身がJSONとかでも良いと思う。最終的にサイズを小さく、
Clientから送るものはフラグだけ、とかになる。
Serverが送り出すものは、Serverで計算なり発行を決めたデータ になる。
つまりRPCな必要が無い。
ゲームごとに制御したい物事、そのインターバルは異なる。
間違っても「定義されているものを使う」という発想にならないほうがいい。
首が絞まってから何かするのは避けてほしい。
Client-Client間でのRPCは、サーバでの統一がしにくい
Client-ClientでRPCを行うのは以下のようなケース。
1.Client A が歩いた
2.Client Aは、歩いたコマンドを発行(コマンドの対象、タイミングはこの時決まる)
3.コマンドがサーバに到達
4.サーバはClient A,B,C,Dに対して、Aが動いたということを通知
5.Client A,B,C,Dのメソッドが着火され、Client Aが動いたのと同じ状態を作り出す
これは、サーバが持つべき「ゲームの統一動作」という責務に対して、最悪の結果を生む。
全Clientがコマンドを受け取ってダイレクトに実行してしまう形になる。
例えば以下の要件は、本来、すべてサーバが担うべきもの。
・Clientが受け取るコマンドの種類を決める
・コマンドの実行可否を決める
・コマンドの実行タイミングを決める
・コマンドの対象を決める
これをClient主導で行うのがなぜダメか。Serverに実行可能かどうかの判断をする場所がなく、実行中の状態が把握できないからだ。
動かす物事のバランスは、「Serverをまずめっちゃ重く」「Clientを軽く」から始めた方がバランスしやすい
最初は計算や描画など、Serverにできない、重たいことをClientにやらせる。
最初からすべてをClientにやらせると、あとでServerにその実装を移す際に困る。
・ClientどうしでRPCするということは、Serverで何もしていない = Serverになんの実装も無い、ということ
・その処理に同期的な処理(アイテム取るとか移動とか攻撃とかダメージとか要はすべて)が必要になった時、ClientがRPCを止めるのが大変。
Client同士でやっていたことを、Serverが行う必要がでてくるが、その実装はゼロから書く羽目になる。
逆にServerが重い状態から始まれば、「チートされても問題の無い箇所」に対して、どこをClientで行うか、という分散はしやすい。
もともとServerでやらないでもいいこと、というのをClientに持ってくるのが正しい。
ほら、RPC無駄だろ?みたいな。
まあ理想論なんだけど。実際には、全部Server側で実装してから~みたいな無駄なことはしないでいいと思う。ただServer側に倒しておくと、あとで救われる。
まだあるRPCの弊害
・チート対策がしづらい
byteで「このClientで実行した」っていうデータを発行してしまうので、偽造されたら一発。
Serverで状態をもっていれば、「この状態だったらこの入力があってもNをしてる最中なので無視!!」とかができる。
・サイズが無駄にでかい
フラグだったら1bitで済むところ、RPCのシリアライゼーションデータを丸っと送る価値って何。
・途中介入がしづらい
Serverが定義していて把握できるbyte列であれば、「先頭数byteをみれば何だかわかる」という条件があると楽。
だが、これはRPCじゃあないよね。
つまりRPCではなく自分でデザインした方がいいよって話。
・実行がメソッド単位で行われてしまう
APIを組み替える時に常にブービートラップのように事件が起こる。
ダイレクトにClientたちの動作が変更されてしまう。仲介役であるServerを絡ませた方がいい。
一度体験すればRPCがどのくらいクソかわかると思うけど、体験させないとダメ?
他のゲームがどうやってるか、どんなFutureを考えているかを見ることで、
ClientベースのRPCが使い物にならない(=実際に使って良いシーンは無い)ことがわかると思う。
できるだけ無駄な体験に時間を費やしたく無い。
じゃあRPCではなくどうすれば良いのか
Server側で、インターバルを持って物事を処理する。
例えばおしくらまんじゅう有りの、位置が同期するゲームの場合は、以下のようなパターンになる。
A:厳格なタイプ
・Clientがフラグを送る(歩く)
・Client側でそのまま歩くモーションになってもいい
・Server側がフラグを受け取る
・Server側のフレームレートで、各プレイヤーから受け取ったフラグを回収
・Server側で一回のフラグに付きどのくらい歩くかを計算、ぶつかりとかもここで計算
・ServerからコマンドをClientに発行
・各Clientがコマンドを受け取り、移動を実行
Server側にすべての計算結果が残るので、再現も中断も楽勝。
加えてキャラクター間のぶつかりとかも計算できる。
敵が死ぬとかはこのフェーズで行わないと困る。
B:あんま厳格でないが速い、失敗しても立て直すタイプ
・Clientがフラグを送る
・Server側がフラグを受け取る
・Serverはフラグから可否のみを計算、フラグを各Clientにできるだけ速く返す
・Serverは裏でClientと同じ計算をする
・Clientはフラグを受け取って動く
・Serverから答え合わせのフレームデータを各Clientに送る
・Clientはそれを受け取って事実を捻じ曲げ再現する
Server側には時間差込みで計算結果が残り、かつ最速で動くことができる。
プレイヤーが少ないほどより効率ダメージを受けず、効果が出る。
実際には、AとBのミックスで設計することが多い。
Client->Server間のRPCについて
フラグ送付ができればいいので、要らないでしょ
それRPCじゃないほうが後々ラクだよね。
なにが悲しくてServer側のメソッドの情報をClientに持たなければいけないのか。
Server->Client間のRPCについて
ClientのVersionに合わせたRPC書くの?
いらなくね?
こちらからは以上です。